package rwt.server;

import java.io.*;

import javax.servlet.http.HttpServletRequest;

import org.apache.log4j.PropertyConfigurator;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.security.*;
import org.eclipse.jetty.security.authentication.FormAuthenticator;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.*;
import org.eclipse.jetty.server.session.*;
import org.eclipse.jetty.server.ssl.SslSelectChannelConnector;
import org.eclipse.jetty.servlet.*;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.webapp.WebAppContext;
import org.slf4j.*;

import rwt.server.core.*;
import rwt.server.core.User.UserRole;
import rwt.server.db.Database;
import rwt.server.layout.TrackLayout;
import rwt.server.mtl.*;
import rwt.server.railroad.*;

/**
 * Main entry point for the Web Throttle Server application.
 */
public class RailServer
{
	private static final Logger logger = LoggerFactory.getLogger(RailServer.class);
	
	private static ClientManager clientManager;
	private static TrainManager trainManager;
	private static RailroadSystem controller;
	private static RailroadSystem railroadServer;
	private static SwitchManager switchManager;
	private static TrackLayout trackLayout;
	private static IMessageAdaptor messageAdaptor;
	
	public static ClientManager getClientManager()
	{
		return clientManager;
	}
	
	public static TrainManager getTrainManager()
	{
		return trainManager;
	}
	
	public static RailroadSystem getController()
	{
		return controller;
	}
	
	public static RailroadSystem getRailroadServer()
	{
		return railroadServer;
	}
	
	public static SwitchManager getSwitchManager()
	{
		return switchManager;
	}
	
	public static TrackLayout getTrackLayout()
	{
		return trackLayout;
	}
	
	public static IMessageAdaptor getMessageAdaptor()
	{
		return messageAdaptor;
	}
	
	public static void main(String[] args) throws Exception
	{
		PropertyConfigurator.configure(Config.CONFIG_DIR_PATH +  "/log4j.properties");
		
		logger.info("Rail Server is starting...");
		// Initialize components
		String configPath = null;
		for (int i = 0; i < args.length - 1; i++)
		{
			if (args[i].equalsIgnoreCase("-config"))
				configPath = args[i + 1];
		}
		
		if (configPath == null)
			logger.warn("Command-line argument '-config' missing.");
		Config.load(configPath);
		
		Database.init();
		
		if (Config.messageTranslationLibrary.equalsIgnoreCase("ada"))
			messageAdaptor = MessageAdaptorFactory.createAdaNativeAdaptor(MessageType.getClassMap());
		else
			messageAdaptor = MessageAdaptorFactory.createPureJavaAdaptor(MessageType.getClassMap());
		
		clientManager = ClientManager.create();
		trainManager = TrainManager.create();
		switchManager = SwitchManager.create();
		
		controller = RailroadSystem.create(Config.controllerExeFile, Config.controllerArgs,
				Config.controllerPort, "controller");
		railroadServer = RailroadSystem.create(Config.railroadExeFile, Config.railroadArgs,
				Config.railroadPort, "railroad");
		
		controller.tryConnect();
		railroadServer.tryConnect();
		
		trackLayout = new TrackLayout();
		trackLayout.load(Config.layoutXmlFile);
		
		if (controller.isConnected())
			switchManager.getSwitchStates();
		
		// Get the configured server port and create the embedded server
		Server server = new Server(8080);
		server.setGracefulShutdown(500);
		server.setStopAtShutdown(true);
		
		// --- We're using HTTPS primarily to work around WWU cross-network web-socket issue ---
		// Encrypt the connection using a valid certificate/keystore
		SslContextFactory sslContextFactory = new SslContextFactory("misc/keystore.jks");
		sslContextFactory.setKeyStorePassword("password");
		
		// Create and add the SslSelectChannelConnector to the server using port specified in config
		SslSelectChannelConnector sslConnector = new SslSelectChannelConnector(sslContextFactory);
		sslConnector.setPort(Config.webServerPort);
		server.setConnectors(new Connector[] { sslConnector });
		
		// Session manager for providing users with unique session IDs
		SessionHandler sessionHandler = new SessionHandler(new HashSessionManager());
		
		// Redirects unauthenticated users to login page and performs user authentication with the database
		SecurityHandler securityHandler = createSecurityHandler();
		ServletHandler servletHandler = new ServletHandler();
		
		// Default servlet (handles requests for static content when forwarded from webPageServlet)
		ServletHolder defaultServlet = new ServletHolder("Default", DefaultServlet.class);
		servletHandler.addServletWithMapping(defaultServlet, "/*.html");
		
		// Main web page servlet (handles JSPs and offloads static files to Default servlet)
		ServletHolder webPageServlet = new ServletHolder(WebPageServlet.class);
		servletHandler.addServletWithMapping(webPageServlet, "/*");
		
		// Web Socket servlet
		servletHandler.addServletWithMapping(WebSocketServlet.class, "/rail/*");
		
		WebAppContext webApp = new WebAppContext(Config.webAppPath + "/secure", "/rwt");
		webApp.setServletHandler(servletHandler);
		webApp.setSessionHandler(sessionHandler);
		webApp.setSecurityHandler(securityHandler);
		webApp.setErrorHandler(new ErrorPageErrorHandler()
		{
			@Override
			protected void writeErrorPage(HttpServletRequest request, Writer writer, int code,
					String message, boolean showStacks) throws IOException
			{
				if (message == null)
					message = HttpStatus.getMessage(code);
				if (code == 403 && message.equals("!role"))
				{
					writer.write("Admin privileges required.");
					return;
				}
				super.writeErrorPage(request, writer, code, message, showStacks);
			}
		});
		
		// Serves static content that is available to the www without user authentication
		ResourceHandler resourceHandler = new ResourceHandler();
		resourceHandler.setBaseResource(Resource.newResource(Config.webAppPath));
		
		// Set the handlers on the server as a handler list
		HandlerList handlers = new HandlerList();
		handlers.setHandlers(new Handler[] { resourceHandler, webApp, new DefaultHandler() });
		server.setHandler(handlers);
		
		// Start the server (launches all handlers)
		server.start();
		
		// If Enter is pressed in console, gracefully shut down the server
		BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
		String line = null;
		while ((line = input.readLine()) != null)
		{
			if (line.equals(""))
			{
				server.stop();
				break;
			}
		}
		
		logger.info("Quitting");
	}
	
	// Creates the security handler. Uses JDBCLoginService to authenticate users with the database.
	private static final SecurityHandler createSecurityHandler() throws IOException
	{
		FormAuthenticator authenticator = new FormAuthenticator("/login.html", "/login_failed.html", false);
		
		// "Basic" security constraint - users must be logged in and have at least Basic role.
		Constraint basicConstraint = new Constraint();
		basicConstraint.setName(Constraint.__BASIC_AUTH);
		basicConstraint.setRoles(new String[] { UserRole.BASIC.getName(), UserRole.MANAGER.getName() });
		basicConstraint.setAuthenticate(true);
		
		ConstraintMapping basicMapping = new ConstraintMapping();
		basicMapping.setConstraint(basicConstraint);
		basicMapping.setPathSpec("/*");
		
		// "Admin" security constraint - users must be logged in and have Admin role.
		Constraint adminConstraint = new Constraint();
		adminConstraint.setName(Constraint.__BASIC_AUTH);
		adminConstraint.setRoles(new String[] { UserRole.MANAGER.getName() });
		adminConstraint.setAuthenticate(true);
		
		ConstraintMapping adminMapping = new ConstraintMapping();
		adminMapping.setConstraint(adminConstraint);
		adminMapping.setPathSpec("/admin/*");
		
		JDBCLoginService loginService = new JDBCLoginService("database_login", Config.CONFIG_DIR_PATH +  "/jdbc_login_config.txt");
		
		ConstraintSecurityHandler sh = new ConstraintSecurityHandler();
		sh.setRealmName("database_login");
		sh.setLoginService(loginService);
		sh.setAuthenticator(authenticator);
		
		sh.setConstraintMappings(new ConstraintMapping[] { adminMapping, basicMapping });
		
		return sh;
	}
}
